﻿using System;
using System.Collections.Concurrent;
using System.Collections.ObjectModel;
using System.Linq;
using System.Net;
using System.Threading;
using SharpShare.Afp.Protocol;
using SharpShare.Diagnostics;
using SharpShare.Storage;
using SharpShare.Storage.Security;
using SharpShare.Utilities.NodeTracking;
using System.Collections.Generic;

namespace SharpShare.Afp {
    internal sealed class AfpShareSession : IShareSession, IAfpSession, ILogProvider {
        public event EventHandler TransportChanged = delegate { };
        public event EventHandler SessionClosed = delegate { };

        private static int CurrentSessionId;

        private AfpTransport _transport;
        private int _currentVolumeId = 200;
        private int _currentForkId = 300;
        private bool _closed;
        private bool _reconnectExpirationSet;
        private DateTime? _dateEnteredSleep;
        private DateTime _dateOpened;
        private Timer _reconnectExpirationTimer;
        private ConcurrentDictionary<ManualResetEvent, string> _pendingMessages = new ConcurrentDictionary<ManualResetEvent, string>();
        private ConcurrentDictionary<ushort, IAfpVolume> _volumes = new ConcurrentDictionary<ushort, IAfpVolume>();
        private ConcurrentDictionary<short, IAfpFork> _forks = new ConcurrentDictionary<short, IAfpFork>();
        private AfpPropertyCollection _properties = new AfpPropertyCollection();
        private AfpReplyCache _replyCache = new AfpReplyCache();
        private TokenAssigner<int, Guid> _userGroupTokenAssigner = new TokenAssigner<int, Guid>(new NumberTokenGenerator(400));
        private object _synclock = new object();

        public AfpShareSession(IAfpServer server, AfpTransport transport) {
            _reconnectExpirationTimer = new Timer(ReconnectExpirationTimerFired, null, -1, -1);
            _dateOpened = DateTime.Now;
            this.Identifier = Interlocked.Increment(ref CurrentSessionId).ToString();
            this.ReconnectTimeout = TimeSpan.FromMinutes(1);
            this.SleepTimeout = TimeSpan.FromDays(1.0);
            this.Server = server;
            this.Transport = transport;
        }

        public string Identifier { get; private set; }
        public IAfpServer Server { get; private set; }
        public AfpTransport Transport {
            get {
                return _transport;
            }
            private set {
                lock (_synclock) {
                    if (_closed) {
                        if (value == null) {
                            return;
                        }

                        throw new InvalidOperationException("Session closed.");
                    }

                    if (_transport == value) {
                        return;
                    }

                    if (_transport != null) {
                        _transport.CommandReceived -= CommandReceived;
                        _transport.CommandSent -= CommandSent;
                        _transport.Closed -= TransportClosed;
                    }

                    _transport = value;

                    if (_transport != null) {
                        _transport.CommandReceived += CommandReceived;
                        _transport.CommandSent += CommandSent;
                        _transport.Closed += TransportClosed;
                        _reconnectExpirationTimer.Change(-1, -1);

                        if (_reconnectExpirationSet) {
                            _reconnectExpirationSet = false;
                            Log.Add(this, EntryType.Information, "Session '{0}' got transport, stopping expiration timer.", this);
                        }

                        this.IsSleeping = false;
                    } else {
                        TimeSpan useTimeout;

                        if (this.IsSleeping) {
                            useTimeout = this.SleepTimeout;
                            Log.Add(this, EntryType.Information, "Sleeping session '{0}' lost transport, closing session in {1}.", this, this.SleepTimeout);
                        } else {
                            useTimeout = this.ReconnectTimeout;
                            Log.Add(this, EntryType.Information, "Session '{0}' lost transport, closing session in {1}.", this, this.ReconnectTimeout);
                        }

                        _reconnectExpirationSet = true;
                        _reconnectExpirationTimer.Change(useTimeout, TimeSpan.FromMilliseconds(-1));
                    }

                    OnTransportChanged();
                }
            }
        }
        public string User { get { return this.AfpUser.Name; } }
        public TimeSpan ReconnectTimeout { get; set; }
        public TimeSpan SleepTimeout { get; set; }
        public string UsingAfpVersion { get; private set; }
        public string UsingUam { get; private set; }

        private IAfpUser AfpUser { get; set; }

        public void Close() {
            lock (_synclock) {
                if (_closed) {
                    return;
                }

                Log.Add(this, EntryType.Information, "Closing session '{0}'...", this);

                _closed = true;
                _reconnectExpirationTimer.Change(-1, -1);

            }

            try {
                if (this.Transport != null) {
                    this.Transport.Close();
                    this.Transport = null;
                }
            } catch { }

            lock (_synclock) {
                foreach (var volumePair in _volumes) {
                    try {
                        volumePair.Value.Close();
                    } catch { }
                }

                foreach (var forkPair in _forks) {
                    try {
                        forkPair.Value.Close();
                    } catch { }
                }
            }

            Log.Add(this, EntryType.Information, "Session '{0}' closed.", this);

            OnSessionClosed();
        }

        public override string ToString() {
            AfpTransport transport = this.Transport;
            return string.Format("{0} (T: {1}, V: {2}, F: {3})",
                this.Identifier,
                (transport == null ? (object)"(none)" : (object)transport),
                _volumes.Count,
                _forks.Count);
        }

        #region Transport Event Handlers

        private void CommandReceived(object sender, AfpTransportCommandReceivedEventArgs eventArgs) {
            AfpTransport transport = (AfpTransport)sender;

            try {
                if (eventArgs.Header.flags == DsiFlags.Reply) {
                    // Unexpected reply?
                    Log.Add(this, EntryType.Warning, "Unhandled reply command '{0}' received, ignoring...", eventArgs.Header);

                    return;
                }

                switch (eventArgs.Header.command) {
                    case DsiCommand.CloseSession:
                        transport.SendReply(eventArgs.Header, AfpResultCode.FPNoErr, new byte[] { });
                        this.Close();

                        break;
                    case DsiCommand.Write:
                    case DsiCommand.Command:
                        // Check reply cache
                        AfpCachedReply reply = _replyCache.DequeueReply(eventArgs.Header.requestId);

                        if (reply != null) {
                            Log.Add(this, EntryType.Information, "Session '{2}' sent cached reply with command code '{0}', request ID: {1}.", reply.Header.command, reply.Header.requestId, this);

                            transport.SendReply(reply.Header, (AfpResultCode)reply.Header.errorCodeOrWriteOffset, reply.Payload);
                            return;
                        }

                        AfpResultCode resultCode = AfpResultCode.FPCallNotSupported;

                        byte commandCode = eventArgs.Payload[0];
                        IAfpRequestHandler handler = AfpRequestHandler.Find(commandCode);

                        AfpStream responseStream = new AfpStream();

                        if (handler != null) {
                            AfpStream requestStream = new AfpStream(eventArgs.Payload);
                            requestStream.ReadUInt8(); // Command code

                            try {
                                try {
                                    resultCode = handler.Process(this, eventArgs.Header, requestStream, responseStream);
                                } catch (Exception ex) {
                                    resultCode = this.Server.HandleCommandException(ex);
                                    responseStream.Stream.SetLength(0);
                                }
                            } catch (Exception ex) {
                                resultCode = AfpResultCode.FPMiscErr;
                                Log.Add(this, EntryType.Error, "Exception while handling '{0}' (command code {1}): {2}", handler.GetType().Name, commandCode, ex);
                            }
                        } else {
                            Log.Add(this, EntryType.Warning, "No handler found for command code {0} (payload size: {1}).", commandCode, eventArgs.Payload.Length);
                        }

                        transport.SendReply(eventArgs.Header, resultCode, responseStream.ToByteArray());

                        break;
                    default:
                        throw new Exception("Unexpected DSI command.");
                }
            } catch (Exception ex) {
                Log.Add(this, EntryType.Error, "Exception while handling command code {1}: {2}", eventArgs.Header.command, ex);
            }
        }

        private void CommandSent(object sender, AfpTransportCommandReceivedEventArgs eventArgs) {
            if (eventArgs.Header.flags != DsiFlags.Reply) {
                return;
            }

            AfpTransport transport = (AfpTransport)sender;

            _replyCache.EnqueueReply(eventArgs.Header, eventArgs.Payload);
        }

        private void TransportClosed(object sender, EventArgs args) {
            this.Transport = null;
        }

        #endregion

        #region IShareSession Members

        ReadOnlyCollection<IOpenFileDescriptor> IShareSession.OpenFiles {
            get {
                var files = _forks.Values
                    .Cast<IOpenFileDescriptor>();

                return files.ToList().AsReadOnly();
            }
        }

        DateTime IShareSession.Date {
            get {
                return _dateOpened;
            }
        }

        #endregion

        #region IAfpSession Members

        IAfpUser IAfpSession.User { get { return AfpUser; } }

        TokenAssigner<int, Guid> IAfpSession.UserGroupAssigner {
            get {
                return _userGroupTokenAssigner;
            }
        }

        IAfpServer IAfpSession.Server {
            get { return this.Server; }
        }

        uint IAfpSession.AttentionQuantum { get; set; }

        int? IAfpSession.Timestamp { get; set; }

        AfpToken? IAfpSession.ServerToken { get; set; }

        AfpToken? IAfpSession.ClientToken { get; set; }

        void IAfpSession.Recover(IAfpSession otherSession) {
            AfpShareSession otherShareSession = (otherSession as AfpShareSession);

            if (otherShareSession == null) {
                throw new ArgumentException("otherSession");
            }

            Log.Add(this, EntryType.Information, "Session {0} recovered transport from {1}.", this, otherSession);

            if (this.Transport != null) {
                this.Transport.Close();
                this.Transport = null;
            }

            AfpTransport otherTransport = otherShareSession.Transport;
            otherShareSession.Transport = null;

            this.Transport = otherTransport;

            otherShareSession.Close();
        }

        AfpPropertyCollection IAfpSession.Properties {
            get {
                return _properties;
            }
        }

        StorageAccessList IAfpSession.GetAccess(IStorageItem item) {
            return StorageAccessList.Empty;
        }


        IAfpFork IAfpSession.OpenFork(IAfpVolume volume, IStorageFile file, AfpAccessModes accessModes, bool resourceFork) {
            short id = GenerateForkId();

            try {
                StorageFileAccess access = StorageFileAccess.None;
                StorageFileAccess share = StorageFileAccess.ReadWrite;

                if ((accessModes & AfpAccessModes.Read) != 0) {
                    access |= StorageFileAccess.Read;
                }
                if ((accessModes & AfpAccessModes.Write) != 0) {
                    access |= StorageFileAccess.Write;
                }
                if ((accessModes & AfpAccessModes.DenyRead) != 0) {
                    share ^= StorageFileAccess.Read;
                }
                if ((accessModes & AfpAccessModes.DenyWrite) != 0) {
                    share ^= StorageFileAccess.Write;
                }

                IStorageDataProvider provider;

                if (resourceFork) {
                    provider = file.OpenResourceFork(access, share, StorageFileMode.OpenOrCreate);
                } else {
                    provider = file.Open(access, share);
                }

                Fork fork = new Fork(id, volume, this, provider, file, access);

                _forks[id] = fork;

                Log.Add(this, EntryType.Debug, "Session {0} opened fork '{1}' (id: {2}, access: {3}, share: {4}).", this, file, id, access, share);

                return fork;
            } catch (UnauthorizedAccessException) {
                return null;
            }
        }

        IAfpFork IAfpSession.GetFork(short id) {
            IAfpFork fork;
            _forks.TryGetValue(id, out fork);
            return fork;
        }

        IEnumerable<IAfpFork> IAfpSession.ListForks() {
            return _forks.Values;
        }

        IAfpVolume IAfpSession.OpenVolume(IStorageProvider provider) {
            ushort id = GenerateVolumeId();
            Volume volume = new Volume(id, this, provider);

            _volumes[id] = volume;

            Log.Add(this, EntryType.Information, "Session '{0}' opened volume '{1}' (id: {2}).", this, volume.StorageProvider.Name, volume.Identifier);

            return volume;
        }

        IAfpVolume IAfpSession.GetVolume(ushort id) {
            IAfpVolume volume;
            _volumes.TryGetValue(id, out volume);
            return volume;
        }

        void IAfpSession.Login(string version, string uam, IAfpUser user) {
            this.AfpUser = user;
            this.UsingAfpVersion = version;
            this.UsingUam = uam;

            Log.Add(this, EntryType.Information, "Session '{3}' logged in (user: {0}, version: {1}, UAM: {2}).", user == null ? "(none)" : user.Name, version, uam, this);
        }

        void IAfpSession.Logout() {
            this.AfpUser = null;
            this.UsingUam = null;
            this.UsingAfpVersion = null;

            Log.Add(this, EntryType.Information, "Session '{0}' logged out.", this);
        }

        Protocol.Security.IAfpUserAuthenticationMethod IAfpSession.AuthenticationMethod { get; set; }

        void IAfpSession.Notify(AfpUserBytes bytes, ushort argument, Action<AfpResultCode> replyHandler) {
            if (this.Transport == null) {
                throw new InvalidOperationException("This session does not currently have a transport.");
            }

            AfpStream stream = new AfpStream();
            stream.WriteUInt16((ushort)((ushort)bytes | argument));

            this.Transport.SendRequest(
                DsiCommand.Attention,
                stream.ToByteArray(),
                (header, payload) => {
                    if (replyHandler != null) {
                        replyHandler((AfpResultCode)header.errorCodeOrWriteOffset);
                    }
                });
        }

        public bool IsSleeping {
            get {
                return _dateEnteredSleep.HasValue;
            }
            set {
                if (!value && _dateEnteredSleep.HasValue) {
                    TimeSpan timeAsleep = (DateTime.Now - _dateEnteredSleep.Value);
                    _dateEnteredSleep = null;

                    this.Transport.TickleMonitoringEnabled = true;

                    Log.Add(this, EntryType.Information, "Client woke up after sleeping for {1} on session '{0}'.", this, timeAsleep);
                } else if (value && !_dateEnteredSleep.HasValue) {
                    _dateEnteredSleep = DateTime.Now;
                    this.Transport.TickleMonitoringEnabled = false;

                    Log.Add(this, EntryType.Information, "Client going to sleep on session '{0}'.", this);
                }
            }
        }

        bool IAfpSession.HasTransport { get { return this.Transport != null; } }

        EndPoint IAfpSession.Endpoint {
            get {
                if (this.Transport != null) {
                    return this.Transport.Socket.RemoteEndPoint;
                }

                return null;
            }
        }

        bool IAfpSession.SendMessage(string message, TimeSpan timeout) {
            using (ManualResetEvent dequeuedEvent = new ManualResetEvent(false)) {
                _pendingMessages[dequeuedEvent] = message;

                ((IAfpSession)this).Notify(AfpUserBytes.MessageAvailable, 0);

                bool signaled = dequeuedEvent.WaitOne(timeout);

                _pendingMessages.TryRemove(dequeuedEvent, out message);

                return signaled;
            }
        }

        string IAfpSession.DequeueMessage(AfpSessionMessageType type) {
            if (type == AfpSessionMessageType.Login) {
                return this.Server.LoginMessage;
            }

            ManualResetEvent dequeuedEvent = _pendingMessages.Keys.FirstOrDefault();
            if (dequeuedEvent != null) {
                string message;
                if (_pendingMessages.TryRemove(dequeuedEvent, out message)) {
                    dequeuedEvent.Set();
                    return message;
                }
            }

            return null;
        }

        #endregion

        #region ILogProvider Members

        string ILogProvider.Name {
            get { return "AFP Session"; }
        }

        #endregion

        private ushort GenerateVolumeId() {
            return (ushort)Interlocked.Increment(ref _currentVolumeId);
        }

        private short GenerateForkId() {
            return (short)Interlocked.Increment(ref _currentForkId);
        }

        private void ReconnectExpirationTimerFired(object state) {
            lock (_synclock) {
                if (this.Transport == null) {
                    Log.Add(this, EntryType.Information, "Session '{0}' reconnect time expired, closing session...", this);
                    this.Close();
                }
            }
        }

        private void OnTransportChanged() {
            try {
                TransportChanged(this, EventArgs.Empty);
            } catch { }
        }

        private void OnSessionClosed() {
            try {
                SessionClosed(this, EventArgs.Empty);
            } catch { }
        }

        private class Volume : IAfpVolume, ILogProvider {
            private TokenAssigner<uint, IStorageItem> _nodeTracker = new TokenAssigner<uint, IStorageItem>(new NumberTokenGenerator(200));

            public Volume(ushort identifier, AfpShareSession session, IStorageProvider provider) {
                this.Identifier = identifier;
                this.Session = session;

                this.StorageProvider = provider;

                this.StorageProvider.ContentsChanged += new EventHandler(StorageProviderContentsChanged);
            }

            public ushort Identifier {
                get;
                private set;
            }

            public AfpShareSession Session {
                get;
                private set;
            }

            IAfpSession IAfpVolume.Session { get { return this.Session; } }

            public IStorageProvider StorageProvider {
                get;
                private set;
            }

            public IStorageItem GetNode(uint id) {
                return _nodeTracker.Get(id);
            }

            public uint GetNode(IStorageItem item) {
                if (item == null) {
                    return AfpConstants.RootParentNodeId;
                }

                if (item.Parent == null) {
                    // Must be root.
                    return AfpConstants.RootNodeId;
                }

                return _nodeTracker.Get(item);
            }

            public void InvalidateNode(IStorageItem item) {
                _nodeTracker.Invalidate(item);
            }

            public void Close() {
                Log.Add(this, EntryType.Information, "Session '{0}' closed volume '{1}' (id: {2}).", this.Session, this.StorageProvider.Name, this.Identifier);

                this.StorageProvider.ContentsChanged -= StorageProviderContentsChanged;

                var forks = this.Session._forks.Values
                    .Where(f => f.Volume == this);
                foreach (var fork in forks) {
                    fork.Close();
                }

                IAfpVolume volume;
                this.Session._volumes.TryRemove(this.Identifier, out volume);
            }

            private void StorageProviderContentsChanged(object sender, EventArgs e) {
                Log.Add(this, EntryType.Debug, "Storage provider for volume '{0}' on session '{1}' contents changed.", this.StorageProvider.Name, this.Session);

                ((IAfpSession)this.Session).Notify(AfpUserBytes.ServerNotification, 1, (code) => {

                });
            }

            #region ILogProvider Members

            string ILogProvider.Name {
                get { return "AFP Volume"; }
            }

            #endregion
        }

        private class Fork : IAfpFork, IOpenFileDescriptor, ILogProvider {
            public Fork(short identifier, IAfpVolume volume, AfpShareSession session, IStorageDataProvider provider, IStorageFile file, StorageFileAccess accessMode) {
                this.DataProvider = provider;
                this.Identifier = identifier;
                this.Session = session;
                this.File = file;
                this.AccessMode = accessMode;
                this.DateOpened = DateTime.Now;
                this.Volume = volume;
            }

            public short Identifier { get; private set; }

            public IAfpVolume Volume { get; private set; }

            public StorageFileAccess AccessMode { get; private set; }

            public AfpShareSession Session { get; private set; }

            public IStorageDataProvider DataProvider { get; private set; }

            public IStorageFile File { get; private set; }

            public void Close() {
                Log.Add(this, EntryType.Debug, "Session '{0}' closed fork '{1}' (id: {2}).", this.Session, this.File, this.Identifier);

                this.DataProvider.Close();

                IAfpFork fork;
                this.Session._forks.TryRemove(this.Identifier, out fork);
            }

            #region IAfpFork Members

            IAfpSession IAfpFork.Session {
                get { return this.Session; }
            }

            #endregion

            #region ILogProvider Members

            string ILogProvider.Name {
                get { return "AFP Fork"; }
            }

            #endregion

            #region IOpenFileDescriptor Members

            string IOpenFileDescriptor.Identifier {
                get {
                    return this.Identifier.ToString();
                }
            }

            public long BytesRead {
                get;
                set;
            }

            public long BytesWritten {
                get;
                set;
            }

            public DateTime DateOpened {
                get;
                set;
            }

            public DateTime DateLastAccessed {
                get;
                set;
            }

            #endregion
        }
    }
}
